/*
 * Copyright (c) 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.samples.apps.iosched.sync.userdata.firebase;

import android.content.Context;
import android.content.Intent;

import com.firebase.client.DataSnapshot;
import com.firebase.client.Firebase;
import com.firebase.client.FirebaseError;
import com.firebase.client.FirebaseException;
import com.google.samples.apps.iosched.gcm.GCMRegistrationIntentService;
import com.google.samples.apps.iosched.sync.userdata.UserAction;
import com.google.samples.apps.iosched.sync.userdata.util.UserData;
import com.google.samples.apps.iosched.sync.userdata.util.UserDataHelper;
import com.google.samples.apps.iosched.util.AccountUtils;
import com.google.samples.apps.iosched.util.FirebaseUtils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.google.samples.apps.iosched.util.LogUtils.LOGI;
import static com.google.samples.apps.iosched.util.LogUtils.LOGW;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;

/**
 * Extracts remote user data from a Firebase {@link DataSnapshot}, which is provided as an argument
 * when this is constructed. Merges remote user data with local user data, and it updates both
 * Firebase and the local DB to ensure data is consistent in both places.
 */
public class FirebaseDataReconciler {
    private static final String TAG = makeLogTag(FirebaseDataReconciler.class);

    /**
     * The {@link UserAction}s that triggered the sync.
     */
    private List<UserAction> mActions;

    /**
     * The remote user data snapshot obtained from Firebase.
     */
    private DataSnapshot mRemoteDataSnapshot;

    /**
     * The context of the current state of the application.
     */
    private Context mContext;

    /**
     * The name of the currently chosen user account.
     */
    private String mAccountName;

    /**
     * Holds user data retrieved from the local {@link android.content.ContentProvider}.
     */
    private UserData mLocalUserData;

    /**
     * Holds user data retrieved from Firebase.
     */
    private UserData mRemoteUserData;

    /**
     * Holds data generated by merging local and remote user data.
     */
    private UserData mMergedUserData;

    /**
     * Helper for merging local and remote data.
     */
    private MergeHelper mMergeHelper;

    /**
     * Constructor.
     *
     * @param context            The {@link Context}.
     * @param accountName        The name associated with the currently chosen account.
     * @param actions            The list of {@link UserAction}s that triggered the sync.
     * @param remoteDataSnapshot The {@link DataSnapshot} of the remote user data stored in
     *                           Firebase.
     */
    public FirebaseDataReconciler(Context context, String accountName, List<UserAction> actions,
            DataSnapshot remoteDataSnapshot) {
        this.mContext = context;
        this.mAccountName = accountName;
        this.mActions = actions;
        this.mRemoteDataSnapshot = remoteDataSnapshot;
    }

    /**
     * Parses the {@link DataSnapshot} object returned by Firebase and sets the remote user data
     * values in a {@link UserData} object.
     *
     * @return this (for method chaining).
     */
    protected FirebaseDataReconciler buildRemoteDataObject() {
        mRemoteUserData = new UserData();
        mRemoteUserData.setGcmKey((String) mRemoteDataSnapshot.child(
                UserData.JSON_ATTRIBUTE_GCM_KEY).getValue());
        mRemoteUserData.setStarredSessions(getRemoteStarredSessionsMap());
        mRemoteUserData.setViewedVideoIds(setRemoteUserDataItem(mRemoteDataSnapshot,
                UserData.JSON_ATTRIBUTE_VIEWED_VIDEOS));

        return this;
    }

    /**
     * Extracts the starred sessions data from a {@link DataSnapshot} and returns the result as a
     * map where the keys are session IDs and the values are {@link UserData.StarredSession}
     * objects.
     */
    private Map<String, UserData.StarredSession> getRemoteStarredSessionsMap() {
        // Store each starred session and the associated StarredSession data in a map.
        Map<String, UserData.StarredSession> starredSessionsMap = new HashMap<>();

        // Get starred sessions snapshot.
        DataSnapshot starredSessionsDataSnapshot = mRemoteDataSnapshot.child(
                FirebaseUtils.FIREBASE_NODE_MY_SESSIONS);

        // Get snapshot of each starred session.
        for (DataSnapshot singleSessionDataSnapshot : starredSessionsDataSnapshot.getChildren()) {
            DataSnapshot inScheduleSnapshot = singleSessionDataSnapshot.child(
                    FirebaseUtils.FIREBASE_NODE_IN_SCHEDULE);
            if (inScheduleSnapshot.getValue() != null) {
                UserData.StarredSession starredSession = new UserData.StarredSession(
                        (boolean) inScheduleSnapshot.getValue(),
                        (Long) singleSessionDataSnapshot.child
                                (FirebaseUtils.FIREBASE_NODE_TIMESTAMP).getValue());
                starredSessionsMap.put(singleSessionDataSnapshot.getKey(), starredSession);
            }
        }
        return starredSessionsMap;
    }

    /**
     * Gets the data stored in the local Db.
     *
     * @return this (for method chaining).
     */
    public FirebaseDataReconciler buildLocalDataObject() {
        mLocalUserData = UserDataHelper.getUserData(mActions);
        mLocalUserData.setGcmKey(AccountUtils.getGcmKey(mContext, mAccountName));
        return this;
    }

    /**
     * Merges user data from the local DB with remote data from Firebase.
     *
     * @return this (for method chaining).
     */
    public FirebaseDataReconciler merge() {
        mMergedUserData = new UserData();
        mMergeHelper = new MergeHelper(mLocalUserData, mRemoteUserData,
                mMergedUserData, FirebaseUtils.getFirebaseUid(mContext));
        mMergeHelper.mergeGCMKeys();
        mMergeHelper.mergeUnsyncedActions(mActions);

        LOGI(TAG, "local user data = " + UserDataHelper.toJsonString(mLocalUserData));
        LOGI(TAG, "remote user data = " + UserDataHelper.toJsonString(mRemoteUserData));
        LOGI(TAG, "merged user data = " + UserDataHelper.toJsonString(mMergedUserData));
        return this;
    }

    /**
     * Updates the remote Firebase db if the remote data has become stale.
     *
     * @return this (for method chaining).
     */
    public FirebaseDataReconciler updateRemote() {
        if (remoteDataChanged()) {
            try {
                Firebase firebaseRef = new Firebase(FirebaseUtils.getFirebaseUrl(mContext,
                        mAccountName));
                firebaseRef.updateChildren(mMergeHelper.getPendingFirebaseUpdatesMap(),
                        new Firebase.CompletionListener() {
                            @Override
                            public void onComplete(final FirebaseError firebaseError,
                                    final Firebase firebase) {
                                if (firebaseError == null) {
                                    LOGI(TAG, "User data updated in Firebase");
                                } else {
                                    LOGW(TAG,
                                            "User data NOT updated in Firebase: firebaseError = " +

                                                    firebaseError);
                                }
                            }
                        });
            } catch (FirebaseException e) {
                LOGW(TAG, "Could not update Firebase: " + e);
            }
        } else {
            LOGI(TAG, "No changes to remote data. Not updating Firebase.");
        }
        return this;
    }

    /**
     * Updates the local DB if the local data has become stale. Updates gcm key in {@link
     * android.content.SharedPreferences}.
     *
     * @return this (for method chaining).
     */
    public FirebaseDataReconciler updateLocal() {
        if (mMergeHelper.isLocalGcmKeyUpdateNeeded()) {
            LOGI(TAG, "Updating local gcm key after sync");
            AccountUtils.setGcmKey(mContext, mAccountName, mMergedUserData.getGcmKey());
            LOGI(TAG, "triggering GCM registration after sync");
            mContext.startService(new Intent(mContext, GCMRegistrationIntentService.class));
        }

        if (localDataChanged()) {
            LOGI(TAG, "Updating local user data after merge.");
            UserDataHelper.setLocalUserData(mContext, mMergedUserData, mAccountName);
        } else {
            LOGI(TAG, "No changes to local data. Not updating ContentProvider.");
        }
        return this;
    }

    /**
     * Returns whether local user data changed after the sync.
     */
    public boolean localDataChanged() {
        return !mMergedUserData.equals(mLocalUserData);
    }

    /**
     * Returns if Firebase data changed after the sync.
     */
    public boolean remoteDataChanged() {
        return !mMergedUserData.equals(mRemoteUserData);
    }

    /**
     * Utility function that parses the {@link DataSnapshot} object returned by Firebase, locates a
     * child DataSnapshot, and sets the value of a single
     * {@link com.google.samples.apps.iosched.sync.userdata.util.UserData} attribute.
     *
     * @param dataSnapshot The {@link DataSnapshot} object returned by Firebase.
     * @param childPath    The path used to find a nested {@link DataSnapshot}.
     * @return A set containing the values found at the {@link DataSnapshot} obtained using {@code
     * childPath}.
     */
    private Set<String> setRemoteUserDataItem(DataSnapshot dataSnapshot, String childPath) {
        Set<String> dataSet = new HashSet<>();
        DataSnapshot childDataSnapshot = dataSnapshot.child(childPath);
        for (DataSnapshot record : childDataSnapshot.getChildren()) {
            dataSet.add(record.getKey());
        }
        return dataSet;
    }
}
